/* schxd.c */

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>

/*-------------*
 * Ring buffer *
 *-------------*/

#ifndef MIN
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
#endif /* MIN */

typedef enum {FALSE, TRUE} boolean;

typedef struct ringbuf_s
{
	size_t writepos, readpos, size;
	unsigned char *pbuf;
	boolean readtip;

} ringbuf_t;

/* returns number of bytes written or zero on error/eof */
size_t RB_FileWrite(ringbuf_t *rb, size_t towrite, FILE *input)
{
	size_t nextwrite, right, writesize, read;
	boolean side_f, nextside_f, overlap_f;

	if (feof(input))
		return (0);

	writesize = MIN(towrite, rb->size);

	side_f = (rb->readpos > rb->writepos ||
		(rb->readtip && rb->readpos == rb->writepos));

	nextwrite = (rb->writepos + writesize) % rb->size;

	overlap_f = (nextwrite < rb->writepos);

	if (overlap_f) {
		right = rb->size - rb->writepos;
		read = fread(rb->pbuf + rb->writepos, 1, right, input);
		if (read == right)
			read += fread(rb->pbuf, 1, nextwrite, input);
	} else {
		read = fread(rb->pbuf + rb->writepos, 1, writesize, input);
	}

	nextwrite = (rb->writepos + read) % rb->size;

	nextside_f = (rb->readpos > nextwrite);
	overlap_f = (nextwrite < rb->writepos);

	if ((side_f != nextside_f && nextwrite > rb->readpos) ||
		(side_f == nextside_f && overlap_f))
		rb->readpos = nextwrite;

	rb->writepos = nextwrite;
	rb->readtip = (read > 0);

	if (ferror(input))
		fprintf(stderr, "error reading file: %s\n", strerror(errno));

	return (read);
}

/* returns number of bytes read */
size_t RB_Read(ringbuf_t *rb, void *pread, size_t requested)
{
	size_t readsize =0;
	int left;

	if (requested > 0 && (rb->readpos != rb->writepos || rb->readtip)) {
		readsize = MIN(requested, ((rb->writepos > rb->readpos) ? 
			(rb->writepos - rb->readpos) :
			(rb->writepos + rb->size - rb->readpos)));

		left = (int) rb->size - (int) rb->readpos - (int) readsize;

		if (left < 0) {
			memcpy(pread, rb->pbuf + rb->readpos, rb->size - rb->readpos);
			memcpy((unsigned char *) pread + rb->size - rb->readpos,
				rb->pbuf, -left);
		} else {
			memcpy(pread, rb->pbuf + rb->readpos, readsize);
		}

		rb->readpos = (rb->readpos + readsize) % rb->size;
		rb->readtip = FALSE;
	}

	return (readsize);
}

/* returns size of data present */
size_t RB_Size(ringbuf_t *rb)
{
	if (rb->readpos != rb->writepos) {
		return ((rb->writepos > rb->readpos) ? (rb->writepos - rb->readpos) :
			(rb->writepos + rb->size - rb->readpos));
	} else {
		return (rb->readtip ? rb->size : 0);
	}
}

/* rewinds read pointer to begin */
void RB_Reset(ringbuf_t *rb)
{
	rb->readpos = rb->writepos;
	rb->readtip = FALSE;
}

/* save/restore read pointer */
typedef struct rbpos_s
{
	size_t readpos;
	boolean readtip;

} rbpos_t;

rbpos_t RB_GetPos(ringbuf_t *rb)
{
	rbpos_t pos = {rb->readpos, rb->readtip};
	return (pos);
}

void RB_SetPos(ringbuf_t *rb, rbpos_t *pos)
{
	rb->readpos = pos->readpos;
	rb->readtip = pos->readtip;
}

/*------*
 * Main *
 *------*/

/* default column width */
#define COLUMN_WIDTH		16
/* number of lines to check column limit */
#define COLUMN_LINES		20
/* maximum search string length */
#define MATCH_LIMIT			16384
/* ring buffer working size */
#define RB_SIZE				(2 * MATCH_LIMIT)

static char hex_chars[16] = {
	'0', '1', '2', '3', '4', '5', '6', '7',
	'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};

/* returns 0 if match is found, 1 otherwise, or above on error */
int main(int argc, char *argv[])
{
	int i, use_hex_f = 0, column_lines = COLUMN_LINES,
		column_width = COLUMN_WIDTH, ignore_case_f = 0,
		result = 0, len, j, uch, line, col_limit,
		col_pos, col_index_f, line_skip_f, match_num,
		found = 0, unget_col, line_num_f = 1, offset_col_f = 1,
		quiet_f = 0, last_switch_f = 0, *param = NULL;
	const char *search_str = 0, *file_name = 0, *prog_name = argv[0];
	FILE *input_dump = 0;
	char *match, *match_upper, ch, buf[256], *parg, *pend = NULL;
	ringbuf_t rbread;
	rbpos_t unget_pos, unget_line, hint_line, cur_pos;

	if (argc > 1) {
		for (i = 1; i < argc; i++) {
			parg = argv[i];
			if (!parg) {
				continue;
			}
			if (!last_switch_f && *parg == '-') {
				if (!strcmp(parg++, "--")) {
					last_switch_f = 1;
					continue;
				}
				while (parg && (ch = *parg++)) {
					switch (ch) {
					case 'x' :	use_hex_f = 1; break;
					case 'i' :	ignore_case_f = 1; break;
					case 'N' :	line_num_f = 0; break;
					case 'A' :	offset_col_f = 0; break;
					case 'q' :	quiet_f = 1; break;
					case 'w' :	param = &column_width; goto loc_getargint;
					case 'c' :	param = &column_lines; goto loc_getargint;
					default :	goto loc_showusage;
loc_getargint:
						if (param) {
							if (!*parg) {
								if (i + 1 < argc) {
									*param = atoi(argv[++i]);
								} else {
									goto loc_showusage;
								}
							} else {
								*param = strtol(parg, &pend, 10);
								if (parg == pend)
									goto loc_showusage;
								parg = pend;
							}
						}
						break;
					}
				}
			} else if (!search_str) {
				search_str = argv[i];
			} else if (!file_name) {
				file_name = argv[i];
			} else {
				goto loc_showusage;
			}
		}
	} else {
loc_showusage:
		fprintf(stderr, "Usage: %s [<switches>] <string> [<hex_dump>]\n"
			"Switches:\n"
			"  -A      \tno offset column\n"
			"  -c <num>\tnumber of lines to check column width, default: %d\n"
			"  -i      \tignore case\n"
			"  -N      \tdon't put line numbers\n"
			"  -q      \tquiet\n"
			"  -w <num>\tset hex column width to <num> bytes, default: %d\n"
			"  -x      \tinterpret <string> as hex bytes, '.' means any hex digit\n"
			,
			prog_name, COLUMN_LINES, COLUMN_WIDTH
			);

		return (EINVAL); /* usage */
	}

	if (!search_str || column_width <= 0 || column_lines <= 0)
		goto loc_showusage;

	input_dump = file_name ? fopen(file_name, "rb") : stdin;

	if (!input_dump) {
		fprintf(stderr, "%s - couldn't open input file: %s\n",
			prog_name, strerror(errno));
		return (errno); /* io error */
	}

	len = (int) strlen(search_str);
	if (!use_hex_f)
		len *= 2; /* plain text will be searched as hex values */

	if (len <= 0 || len > (MATCH_LIMIT - 1)) {
		fprintf(stderr, "%s - bogus search string\n", prog_name);
		result = EINVAL; /* usage */
		goto loc_exit_file;
	}

	match = malloc(len/* add trailing zero */+ 1);
	match_upper = malloc(len/* add trailing zero */+ 1);

	memset(&rbread, 0, sizeof (rbread));
	rbread.size = RB_SIZE;
	rbread.pbuf = malloc(rbread.size);

	if (!match || !match_upper || !rbread.pbuf) {
		fprintf(stderr, "%s - memory allocation failed: %s\n",
			prog_name, strerror(errno));
		result = ENOMEM; /* memory */
		goto loc_exit_file;
	}

	RB_Reset(&rbread);

	/* fill in match string as hex bytes */
	for (j = i = 0; i < len && search_str[i]; i++) {
		ch = search_str[i];

		if (use_hex_f) {
			if (isspace(ch)) /* use isblank()? */
				continue;

			if (ch != '.' && !isxdigit((unsigned char) ch)) {
				fprintf(stderr, "%s - bogus search string\n", prog_name);
				result = EINVAL; /* usage */
				goto loc_exit_alloc;
			}

			match[j] =(char) tolower(ch);
			match_upper[j++] =(char) toupper(ch);
		} else {
			uch = ch;

			if (ignore_case_f)
				ch = tolower(ch);

			uch = toupper(uch);

			match[j] = hex_chars[((ch >> 4) & 0x0f)];
			match_upper[j++] = hex_chars[((uch >> 4) & 0x0f)];
			match[j] = hex_chars[(ch & 0x0f)];
			match_upper[j++] = hex_chars[(uch & 0x0f)];
		}
	}

	if (j == 0) {
		fprintf(stderr, "%s - bogus search string\n", prog_name);
		result = EINVAL; /* usage */
		goto loc_exit_alloc;
	}

	/* force trailing zero */
	match[j] = 0;
	match_upper[j] = 0;
	
	/* determine hex column right bound */
	col_pos = 0;
	line = 0;
	col_limit = 0;

	if (!RB_FileWrite(&rbread, RB_SIZE, input_dump)) {
		result = EIO; /* io error */
		goto loc_exit_alloc;
	}

	cur_pos = RB_GetPos(&rbread);

	while (RB_Read(&rbread, &ch, 1) && line < column_lines) {
		if (ch != '\n' && col_pos < sizeof (buf)) {
			buf[col_pos++] =(char) ch;
		} else {
			col_index_f = offset_col_f;

			for (len = j = i = 0; i < col_pos &&
				(isxdigit((unsigned char) buf[i]) || buf[i] == ' '); i++) {
				if (col_index_f) {
					if (buf[i] == ' ')
					col_index_f = 0;
				} else if (buf[i] != ' ') {
					if (j % 2) {
						len++;

						if (len == column_width) {
							if (i > col_limit) {
								col_limit = i + 1;
								break;
							}
						}
					}

					j++;
				}
			}

			col_pos = 0;
			line++;
		}
	}

	RB_SetPos(&rbread, &cur_pos);

	match_num = line = 1; /* line starts with 1 */
	j = 0;
	unget_col = col_pos = 0;
	col_index_f = offset_col_f;
	line_skip_f = 0;
	found = 0;
	unget_line = hint_line = RB_GetPos(&rbread);

loc_datafetched:
	while (RB_Read(&rbread, &ch, 1)) {
		if (ch != '\n') {
			if (line_skip_f)
				continue;

			if (!isxdigit((unsigned char) ch) && ch != ' ') {
				line_skip_f = 1;
				continue;
			}

			if (col_pos > col_limit)
				continue;

			col_pos++;

			if (!col_index_f) {
				if (ch == ' ')
					continue;

				if (match[j] == ch || (match_upper[j] == ch &&
					(use_hex_f || ignore_case_f)) || (match[j] == '.' && use_hex_f)) {

					if (j == 0) {
						/* save position to rewind in case of incomplete match */
						unget_pos = RB_GetPos(&rbread);
						hint_line = unget_line;
						unget_col = col_pos;
						match_num = line;
					}

					j++;

					if (match[j] == 0) {
						j = 0; /* reset match counter */

						found++;

						if (quiet_f)
							goto loc_exit_alloc;

#define HINT_SIZE	(sizeof (buf) - 1)
						/* print match starting line hint */
						if (RB_Size(&rbread) < HINT_SIZE)
							RB_FileWrite(&rbread, HINT_SIZE, input_dump);

						cur_pos = RB_GetPos(&rbread);

						RB_SetPos(&rbread, &hint_line);
						len = (int) RB_Read(&rbread, buf, HINT_SIZE);

						for (i = 0; i < len && buf[i] != '\n'; i++);

						buf[i] = 0;

						RB_SetPos(&rbread, &cur_pos);

						if (line_num_f)
							printf("%d:%s\n", match_num, buf);
						else 
							printf("%s\n", buf);
					}
				} else if (j > 0) {
					/* rewind to saved on starting match position */
					RB_SetPos(&rbread, &unget_pos);

					line = match_num;
					col_pos = unget_col;
					j = 0;
				}
			} else if (ch == ' ') {
				col_index_f = 0;
				continue;
			}
		} else {
			col_pos = 0;
			col_index_f = offset_col_f;
			line_skip_f = 0;
			line++;
		}

		if (!col_pos) /* save line begin position */
			unget_line = RB_GetPos(&rbread);
	}

	if (RB_FileWrite(&rbread, MATCH_LIMIT, input_dump))
		goto loc_datafetched;

	if (!quiet_f)
		printf("Found %d matches\n", found);

loc_exit_alloc:
	free(rbread.pbuf);
	free(match_upper);
	free(match);

loc_exit_file:
	if (file_name) /* otherwise it was standard input */
		fclose(input_dump);

	/* FIXME: EPERM masked */
	return ((result > 1) ? result : (found == 0));
}

/* end */
